import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/***
 * This is a small utility for generate KCachegrind recognizable
 * output from either jstack output or the kill -QUIT pid output of the
 * JVM. 
 * You can generate java stack samples every 10 seconds by running the following command
 *  while true ; do /path_to_your_jdk_bin/jstack [PID] >> jstack.log ; sleep 10 ; done
 * after that you can run this tool against jstack.log:
 * java -cp . JCallGrind jstack.log
 * you will get callgrind.[thread_name].out files, then you can open them in 
 * KCachegrind (http://kcachegrind.sourceforge.net/html/Home.html, my windows port:
 * http://sourceforge.net/projects/precompiledbin/files/kcachegrind.zip/download

 * @author Lailin Chen
 * 
 * This tool is released under MIT license
 * http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * A quick document about Callgrind output:
 
    events: ir      -> counts of instructions. 
    fl=file.f       -> what is the source file name
    fn=main         -> what function in the source file
    15 90 14 2      -> at line 15 of file.f, inside main, 90 events passed by, total 14 instructions, 2 of them floating point instructions
    16 20 12        -> at line 16 of file.f, inside main, 20 events passed by, total 12 instructions, 0 of them floating point instructions
    cfl=file2.f     -> the following called functions are inside file2.f
    cfn=func1       -> main called function func1
    calls=1 50      -> func1 was called 1 times, the destination line in  file1.f is 50 
    16 400          -> The call happened at line 16 of file.f, inside function main, the inclusive cost is 400 events.
   
    The calls section is very important, a simple summary:
     calls=(Call Count) (Destination position)
     (Source position) (Inclusive cost of call)
 */
public class JCallGrind 
{
    
    public static void main(String[] args) throws IOException 
    {
        if(args.length < 1)
        {
            System.out.println("Please specify a input file name");
            return;
        }
        File file = new File(args[0]);
        BufferedReader reader = null;
        JCallFrame allroot = new JCallFrame();
        HashMap<String, String> threaNames = new HashMap<String, String>();
        try
        {
            reader = new BufferedReader(new FileReader(file));
            String text = null;
            Pattern tidPattern =  null;
            String prefixToken = null;
            int skipPrefix = 0; 
            JCallFrame.FrameType frameType;
            
            if(args.length == 2 && args[1].equals("true"))
            {
                prefixToken = "- ";
                skipPrefix = 2;
                tidPattern = Pattern.compile("Thread ([0-9]+): \\(state = [_A-Z]+\\)");
                frameType = JCallFrame.FrameType.SAFrame;
            }
            else
            {
                prefixToken = "at ";
                skipPrefix = 3;
                tidPattern = Pattern.compile("tid=([^ ]+)");
                frameType = JCallFrame.FrameType.JStackFrame;
            }
            // repeat until all lines is read
            while ((text = reader.readLine()) != null)
            {
                //skip empty lines
                while(text != null && text.length() == 0)
                {
                    text = reader.readLine();
                }
                if(text == null)
                {
                    break;
                }
                String currentTid = "unknown";
                String threadName = currentTid;
                Matcher m = tidPattern.matcher(text);
                if(m.find())
                {
                    String tid = m.group(1);
                    currentTid = tid;
                    // e.g. "Java2D Disposer" daemon prio=10 tid=0xa203c400 nid=0x12f4 in Object.wait() [0xa21eb000]
                    // or Thread 4846: (state = BLOCKED) 
                    int threadNameEnd =  text.indexOf("\"", 1);
                    if(threadNameEnd > 0)
                    {
                        threadName = text.substring(1,threadNameEnd);
                    }
                    else
                    {
                        threadName = currentTid;
                    }
                    threaNames.put(tid, threadName);
                }
                Stack<String> currentCallStack = new Stack<String>();
                do
                {
                    text = reader.readLine();
                    if(text == null) break; // EOF
                    text = text.trim();
                    if(text.startsWith(prefixToken))
                    {
                        currentCallStack.push(text.substring(skipPrefix, text.length()));
                    }
                 
                }while(!(text.length() == 0));
                
                // Filter out idle samples. 
                if(!currentCallStack.empty())
                {
                    String frameText = currentCallStack.firstElement();
                    String functionName = frameText.substring(0,frameText.indexOf(')')+1);
                    if(!idleFunctions.contains(functionName))
                    {
                        JCallFrame jcf = allroot.addChild(currentTid);
                        while(!currentCallStack.empty())
                        {
                            String currentFrame = currentCallStack.pop();
                            jcf = jcf.addChildFromText(currentFrame, frameType);
                        }
                    }
                }
            }
        } catch (FileNotFoundException e) {
            System.err.println("File not found!");
        } catch (IOException e) {
            System.err.println("Failed open file!");
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        allroot.getfChildren().remove(null);
        witeCallgrindCompatibleCallGraph(allroot,threaNames);
        // Now generate a special call graph by combining all the threads
        if(allroot.getfChildren().size() > 0)
        {
            threaNames.put("AllthreadsConbined", "AllthreadsConbined");
            JCallFrame allThreadsConbinedRoot = new JCallFrame();
            allThreadsConbinedRoot.getfChildren().put("AllthreadsConbined", allroot);
            generateCallGraphForThread(allThreadsConbinedRoot, threaNames, "AllthreadsConbined");
        }
     }


    private static void initFrame(JCallFrame jcf, String currentFrame)
    {

    }


    private static void printHeaders(PrintWriter writer, String threadName)
    {
        writer.println("version: 1");
        writer.println("creator: jcallgrind 1.0");
        writer.println("pid: " + threadName);
        writer.println("cmd:  " + "java");
        writer.println("part: 1");
        writer.println(""); 
        writer.println("desc: I1 cache:");
        writer.println("desc: D1 cache:"); 
        writer.println("desc: L2 cache:"); 
        writer.println("");
        writer.println("positions: line");
    }
    
    
    public static void witeCallgrindCompatibleCallGraph(JCallFrame root,
            HashMap<String, String> threaNames) throws IOException
    {
        for (String tid : root.getfChildren().keySet()) 
        {
            generateCallGraphForThread(root, threaNames, tid);
        }
    }


    private static void generateCallGraphForThread(JCallFrame root,
            HashMap<String, String> threaNames, String tid) throws IOException
    {
        String fileName = "callgrind." + threaNames.get(tid) + ".out";
        fileName = URLEncoder.encode( fileName , "UTF-8");
        System.out.println("generating file: " + fileName);
        FileWriter out = new FileWriter(fileName);
        BufferedWriter bufferedWriter = new BufferedWriter(out);
        PrintWriter writer = new PrintWriter(bufferedWriter);
        printHeaders(writer, tid);
        writer.println("events: Ir(samples)");
        writer.println("");
        NameCache fileNameCache = new NameCache();
        NameCache functionNameCache = new NameCache();
        int subtotal = dumpThreadCallTree(root.getfChildren().get(tid), writer, tid, true, fileNameCache, functionNameCache);
        writer.println("totals: " + String.valueOf(subtotal));
        writer.flush();
        out.close();
    }
    private static int dumpThreadCallTree(JCallFrame jcf, 
                                            PrintWriter writer,
                                            String jcfName, 
                                            boolean skipCurrentFrame,
                                            NameCache fileNameCache,
                                            NameCache functionNameCache)
    {
        int subtotal = 0;
        if(!skipCurrentFrame)
        {
            int childrenCount = 0;
            for(JCallFrame jcfchild : jcf.getfChildren().values())
            {
                childrenCount += jcfchild.getfCount();
            }
            int selfCount = jcf.getfCount() - childrenCount;
            writer.println("fl=" + fileNameCache.getName(jcf.getSourceFileName()));
            writer.println("fn=" + functionNameCache.getName(jcf.getCurrentFunctionName()));
            writer.println(jcf.getCurrentLineNumber() + " " + selfCount);
            subtotal += selfCount;
            for(String childName : jcf.getfChildren().keySet())
            {
                JCallFrame childCf = jcf.getfChildren().get(childName);
                writer.println("cfl=" + fileNameCache.getName(childCf.getSourceFileName()));
                writer.println("cfn=" + functionNameCache.getName(childCf.getCurrentFunctionName()));
                writer.println("calls=" + childCf.getfCount() + " " + childCf.getCurrentLineNumber());
                writer.println(jcf.getCurrentLineNumber() + " " + childCf.getfCount());
            }
        }
        for(String childName : jcf.getfChildren().keySet())
        {
            subtotal += dumpThreadCallTree(jcf.getfChildren().get(childName), writer, childName, false, fileNameCache, functionNameCache);
        }
        return subtotal;
    }
    
    private static Set<String> idleFunctions = new HashSet<String>();
    // Initialize 
    static {
        // JStack 
        idleFunctions.add("sun.misc.Unsafe.park(Native Method)"); 
        idleFunctions.add("java.lang.Object.wait(Native Method)");
        idleFunctions.add("java.lang.Thread.sleep(Native Method)");
        
        // SA
        idleFunctions.add("sun.misc.Unsafe.park(boolean, long)"); 
        idleFunctions.add("java.lang.Object.wait(long)");
        idleFunctions.add("java.lang.Thread.sleep(long)");
    }
    
}